孤舟蓑笠翁,独钓寒江雪

React Native封装原生UI组件

在React Native开发过程中,有时我们想要使用原生的一个UI组件或者是JS比较难以实现的动画效果时,我们可以在React Naitve应用程序中封装和植入已有的原生组件。
比如开源项目Lottie在Android上能够非常简单的实现一些复杂的动画效果,如果我们想在JS中也实现这样的效果呢?很简单,我们可以自己构建一个原生UI组件。

接下来就以此为例来进行介绍。Lottie官方已经提供了React Native版本的Lottie了,但这里我们作为例子再来介绍一下。
官方中文文档
先看一下效果图:

封装组件

创建ViewManager的子类

创建LottieViewManager类,并实现getName()createViewInstance方法:

1
2
3
4
5
6
7
8
9
10
11
12
public class LottieViewManager extends SimpleViewManager<LottieAnimationView> {
private static final String REACT_CLASS = "LottieAnimationView";
@Override
public String getName() {
return REACT_CLASS;
}

@Override
protected LottieAnimationView createViewInstance(ThemedReactContext reactContext) {
return new LottieAnimationView(reactContext);
}
}

导出属性的设置方法

要导出给JavaScript使用的属性,需要申明带有@ReactProp(或@ReactPropGroup)注解的设置方法。方法的第一个参数是要修改属性的视图实例,第二个参数是要设置的属性值。方法的返回值类型必须为void,而且访问控制必须被声明为public。JavaScript所得知的属性类型会由该方法第二个参数的类型来自动决定。支持的类型有:boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap
@ReactProp注解必须包含一个字符串类型的参数name。这个参数指定了对应属性在JavaScript端的名字。
除了name@ReactProp注解还接受这些可选的参数:defaultBoolean, defaultInt, defaultFloat。这些参数必须是对应的基础类型的值(也就是boolean, int, float),这些值会被传递给setter方法,以免JavaScript端某些情况下在组件中移除了对应的属性。注意这个”默认”值只对基本类型生效,对于其他的类型而言,当对应的属性删除时,null会作为默认值提供给方法。
使用@ReactPropGroup来注解的设置方法和@ReactProp不同。请参见@ReactPropGroup注解类源代码中的文档来获取更多详情。
注意: 在ReactJS里,修改一个属性会引发一次对设置方法的调用。有一种修改情况是,移除掉之前设置的属性。在这种情况下设置方法也一样会被调用,并且“默认”值会被作为参数提供(对于基础类型来说可以通过defaultBooleandefaultFloat@ReactProp的属性提供,而对于复杂类型来说参数则会设置为null
LottieViewManager中创建以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ReactProp(name = "sourceName")
public void setSourceName(LottieAnimationView view, String name) {
view.setAnimation(name);
}

@ReactProp(name = "progress", defaultFloat = 0f)
public void setProgress(LottieAnimationView view, float progress) {
view.setProgress(progress);
}

@ReactProp(name = "loop")
public void setLoop(LottieAnimationView view, boolean loop) {
view.loop(loop);
}

这里方法名可以随意定,但是@ReactProp(name = "loop")一定要和JS里面调用的属性值对应。

导出一些命令

LottieViewManager中添加以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private static final int COMMAND_PLAY = 1;
private static final int COMMAND_RESET = 2;

@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of(
"play", COMMAND_PLAY,
"reset", COMMAND_RESET
);
}

@Override
public void receiveCommand(final LottieAnimationView view, int commandId, @Nullable ReadableArray args) {
switch (commandId) {
case COMMAND_PLAY: {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override public void run() {
if (ViewCompat.isAttachedToWindow(view)) {
view.setProgress(0f);
view.playAnimation();
}
}
});
}
break;
case COMMAND_RESET: {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override public void run() {
if (ViewCompat.isAttachedToWindow(view)) {
view.cancelAnimation();
view.setProgress(0f);
}
}
});
}
break;
}
}

在js文件可以通过this.runCommand('play')来调用。

注册ViewManager

实现ReactPackage的子类LottiePackage,这和原生模块的注册方法类似,唯一的区别是我们把它放到createViewManagers方法里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LottiePackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new LottieViewManager()
);
}
}

添加组件

ApplicationgetPackages()方法中添加模块:

1
2
3
4
5
6
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(), new LottiePackage()
);
}

或者是在ActivityonCreate中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mReactRootView = new ReactRootView(this);
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(getApplication())
.setBundleAssetName("index.android.bundle")
.setJSMainModuleName("index.android")
.addPackage(new MainReactPackage())
.addPackage(new LottiePackage())
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
mReactRootView.startReactApplication(mReactInstanceManager, "HelloWorld", null);
setContentView(mReactRootView);
}

实现对应的JavaScript模块

创建Lottie.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React, { PropTypes } from 'react';
import {
requireNativeComponent,
View,
UIManager,
findNodeHandle,
ReactNative,
Platform } from 'react-native';

/*
var LottieView = {
name: 'LottieView',
defaultProps: {
progress: 0,
loop: true,
},
propTypes: {
sourceName :PropTypes.string,
progress: PropTypes.number,
loop: PropTypes.bool,
...View.propTypes // 包含默认的View的属性
},
};

module.exports = requireNativeComponent('LottieAnimationView', LottieView);
*/

const Lottie = requireNativeComponent('LottieAnimationView', LottieView);

class LottieView extends React.Component {
constructor(props) {
super(props);
}

play() {
this.runCommand('play');
}

reset() {
this.runCommand('reset');
}

runCommand(name, args = []) {
return Platform.select({
android: () => UIManager.dispatchViewManagerCommand(
this.getHandle(),
UIManager.LottieAnimationView.Commands[name],
args
),
ios: () => LottieViewManager[name](this.getHandle(), ...args),
})();
}
getHandle() {
return findNodeHandle(this.refs.lottieView);
}

render() {
return <Lottie ref="lottieView" {...this.props} loop={true} />;
}
}

module.exports = LottieView;

UIManager.dispatchViewManagerCommand把调用命令分发到Native端对应的组件类型的ViewManager,再通过ViewManager调用View组件实例的对应方法,这部分后面再介绍。

调用组件

在index.android.js里面调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import LottieView from './Lottie'
……
onClicked(){
this.refs.lottie.play();
}
render() {
return (
<TouchableWithoutFeedback onPress={() => this.onClicked()}>
<View style={styles.lottieContainer} >
<LottieView ref="lottie" style={styles.lottie} sourceName='LogoSmall.json' loop={true}>
</LottieView>
</View>
</TouchableWithoutFeedback>

)
}

LogoSmall.json 文件可以从LottieAndroid的源码中获取,点击这里,放在 src/main/assets 即可。